#!/bin/python3
# _*_ coding: utf-8 _*_
#    NGINX Config 
#    proxy_read_timeout 3600s;
#    client_max_body_size 0;
#    proxy_request_buffering off;


from xml.etree import ElementTree as ET
import argparse
import socket
import socketserver
import http.server
import os
from urllib.parse import urlparse, unquote
import shutil
import uuid
import re
import platform
import mimetypes
from xml.sax.saxutils import escape
import struct
import fcntl
import base64
from functools import wraps
import time
from urllib.parse import quote



def get_interface_ip(ifname, family):
    """Retrieve the IP address associated with a network interface."""
    s = socket.socket(family, socket.SOCK_DGRAM)
    try:
        if family == socket.AF_INET:  # IPv4
            return socket.inet_ntoa(fcntl.ioctl(
                s.fileno(),
                0x8915,  # SIOCGIFADDR for IPv4
                struct.pack('256s', bytes(ifname[:15], 'utf-8'))
            )[20:24])
        elif family == socket.AF_INET6:  # IPv6
            return socket.inet_ntop(
                socket.AF_INET6,
                fcntl.ioctl(
                    s.fileno(),
                    0x8915,  # SIOCGIFADDR for IPv6 might need to be different
                    struct.pack('256s', bytes(ifname[:15], 'utf-8'))
                )[20:36]
            )
    except IOError:
        return None

def list_all_ips():
    """List all IP addresses for all network interfaces (IPv4 and IPv6)."""
    addresses = []
    interfaces = socket.if_nameindex()

    for interface in interfaces:
        # Get IPv4 address for the interface
        ipv4 = get_interface_ip(interface[1], socket.AF_INET)
        if ipv4 and ipv4 != '127.0.0.1':
            addresses.append((interface[1], ipv4, 'IPv4'))

        # Get IPv6 address for the interface
        ipv6 = get_interface_ip(interface[1], socket.AF_INET6)
        if ipv6 and not ipv6.startswith('fe80'):  # Filter out link-local addresses
            addresses.append((interface[1], ipv6, 'IPv6'))

    return addresses

class ForkingHTTPServer(socketserver.ForkingTCPServer):
    allow_reuse_address = 1
    address_family = socket.AF_INET6

    def server_bind(self):
        socketserver.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()[:2]
        self.server_name = socket.getfqdn(host)
        self.server_port = port

class CustomHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):

    _auth_enabled = False
    _credentials = ('', '')  # 存储(user, password)



    def require_auth(f):
        """身份验证装饰器"""
        @wraps(f)
        def wrapper(self, *args, **kwargs):
            if not self._auth_enabled:
                return f(self, *args, **kwargs)
                
            auth_header = self.headers.get('Authorization', '')
            if not auth_header.startswith('Basic '):
                self._send_auth_challenge()
                return

            try:
                # 解析Base64凭证
                auth_decoded = base64.b64decode(auth_header[6:]).decode('utf-8')
                user, passwd = auth_decoded.split(':', 1)
                if (user, passwd) == self._credentials:
                    return f(self, *args, **kwargs)
            except Exception:
                pass
            
            self._send_auth_challenge()
        return wrapper

    def _send_auth_challenge(self):
        """发送401认证质询"""
        self.send_response(401)
        self.send_header('WWW-Authenticate', 'Basic realm="HTTP Server", charset="UTF-8"')
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(b'<h1>Authentication Required</h1>')

    def safe_content_disposition(self, filename):
        """确保 Content-Disposition 头部支持 UTF-8 文件名"""
        try:
            filename.encode("ascii")  # 检查是否为 ASCII
            return f'attachment; filename="{filename}"'
        except UnicodeEncodeError:
            filename_utf8 = quote(filename, encoding="utf-8")
            return f"attachment; filename*=UTF-8''{filename_utf8}"

    def do_GET(self):
        """处理目录和文件请求：目录请求生成索引，文件请求下载"""
        filepath = self.translate_path(unquote(self.path))  # 解析 URL 编码

        # 如果请求的是一个目录，使用list_directory方法生成目录索引
        if os.path.isdir(filepath):
            return self.list_directory(filepath)  # 调用父类的方法列出目录内容
        
        # 否则处理文件请求
        if not os.path.exists(filepath):
            self.send_error(404, "File not found")
            return

        try:
            file_size = os.path.getsize(filepath)
            mime_type, _ = mimetypes.guess_type(filepath)
            if not mime_type:
                mime_type = "application/octet-stream"

            range_header = self.headers.get("Range")
            if range_header:
                range_match = re.match(r'bytes=(\d+)-(\d*)', range_header)
                if range_match:
                    start = int(range_match.group(1))
                    end = range_match.group(2)
                    if end:
                        end = int(end)
                    else:
                        end = file_size - 1
                else:
                    start = 0
                    end = file_size - 1

                if start >= file_size:
                    self.send_error(416, "Requested Range Not Satisfiable")
                    return

                self.send_response(206)
                self.send_header("Content-Type", mime_type)
                self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
                self.send_header("Content-Length", str(end - start + 1))
                self.send_header("Accept-Ranges", "bytes")
                self.send_header("Content-Disposition", self.safe_content_disposition(os.path.basename(filepath)))
                self.end_headers()

                with open(filepath, "rb") as f:
                    f.seek(start)
                    remaining = end - start + 1
                    buffer_size = 8192
                    while remaining > 0:
                        read_size = min(buffer_size, remaining)
                        data = f.read(read_size)
                        if not data:
                            break
                        self.wfile.write(data)
                        remaining -= len(data)
            else:
                self.send_response(200)
                self.send_header("Content-Type", mime_type)
                self.send_header("Content-Length", str(file_size))
                self.send_header("Accept-Ranges", "bytes")
                self.send_header("Content-Disposition", self.safe_content_disposition(os.path.basename(filepath)))
                self.end_headers()

                with open(filepath, "rb") as file:
                    shutil.copyfileobj(file, self.wfile, length=8192)
        except Exception as e:
            self.send_error(500, f"Internal Server Error: {str(e)}")


    def send_head(self):
        """Override the default method to handle Range requests."""
        path = self.translate_path(self.path)
        
        if os.path.isdir(path):
            index = os.path.join(path, 'index.html')
            if os.path.exists(index):
                path = index  # Serve index.html if it exists
            else:
                return self.list_directory(path)  # List directory if no index.html
    
        try:
            file = open(path, 'rb')
        except OSError:
            self.send_error(404, "File not found")
            return None

        file_size = os.path.getsize(path)
        range_header = self.headers.get('Range')
    
        if range_header:
            # Parsing the Range header, e.g., bytes=500-999
            range_match = re.match(r"bytes=(\d+)-(\d*)", range_header)
            if range_match:
                start = int(range_match.group(1))
                end = range_match.group(2)
                end = int(end) if end else file_size - 1
    
                if start >= file_size:
                    self.send_error(416, "Requested Range Not Satisfiable")
                    self.send_header("Content-Range", f"bytes */{file_size}")
                    self.end_headers()
                    return None
    
                self.send_response(206)
                self.send_header("Content-Type", self.guess_type(path))
                self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}")
                self.send_header("Content-Length", str(end - start + 1))
                self.send_header("Accept-Ranges", "bytes")
                self.end_headers()
    
                # Serve the requested file range
                file.seek(start)
                self.wfile.write(file.read(end - start + 1))
                file.close()
                return None
    
        # Default response when no Range request is made (serve the full file)
        self.send_response(200)
        self.send_header("Content-Type", self.guess_type(path))
        self.send_header("Content-Length", str(file_size))
        self.send_header("Accept-Ranges", "bytes")
        self.end_headers()
    
        return file
    


    def list_directory(self, path):
        try:
            directory_list = os.listdir(path)
        except OSError:
            self.send_error(404, "No permission to list directory")
            return None

        directory_list.sort(key=lambda a: a.lower())

        # Get full current directory for title (relative to root)
        full_directory = os.path.relpath(os.path.abspath(path), start=os.getcwd())

        # Create modern HTML structure with improved design
        html = f"""<!DOCTYPE html>
        <html lang="zh-CN">
        <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{full_directory} - 文件列表 / File List</title>  <!-- Full directory in title -->
        <style>
        * {{
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }}
        body {{
            font-family: 'Arial', sans-serif;
            background-color: #f4f4f9;
            color: #333;
            line-height: 1.6;
            padding: 20px;
        }}
        h2 {{
            text-align: center;
            margin-bottom: 20px;
            color: #333;
        }}
        table {{
            width: 100%;
            max-width: 1200px;
            border-collapse: collapse;
            margin: 0 auto;
            background-color: white;
            border-radius: 8px;
            overflow: hidden;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }}
        th, td {{
            padding: 8px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }}
        th {{
            background-color: #007bff;
            color: white;
        }}
        tr:hover {{
            background-color: #f1f1f1;
        }}
        a {{
            text-decoration: none;
            color: #007bff;
        }}
        a:hover {{
            text-decoration: underline;
        }}
        footer {{
            text-align: center;
            margin-top: 20px;
        }}
        @media (max-width: 600px) {{
            table, th, td {{
                display: block;
                width: 100%;
            }}
            th, td {{
                padding: 10px;
            }}
        }}
        </style>
        </head>
        <body>
        <h2>{full_directory} - 文件列表 / File List</h2>  <!-- Full directory in heading -->
        <table>
        <tr><th>名称 / Name</th><th>大小 / Size</th></tr>
        """

        # Insert `../` for going back to parent directory
        if self.path != '/':
            html += f"""<tr><td><a href="../">../ 返回上一级 / Go Back</a></td><td></td></tr>"""

        # Populate file list with file size
        for name in directory_list:
            fullname = os.path.join(path, name)
            display_name = link_name = name
            if os.path.isdir(fullname):
                display_name = name + "/"
                link_name = name + "/"
                size = ""
            if os.path.islink(fullname):
                display_name = name + "@"
                size = ""
            else:
                size = self.format_size(os.path.getsize(fullname))  # Format file size

            html += f"""<tr><td><a href="{link_name}">{display_name}</a></td><td>{size}</td></tr>"""

        # Add footer with project link
        html += """
        </table>
        <footer>
        <p>项目链接 / Project link: <a href="https://gitee.com/shenmo7192/momo-and-mox-tool-scripts/blob/master/server.py">Momo and Mox Tool Scripts</a></p>
        </footer>
        </body>
        </html>
        """

        # Encode the HTML content
        encoded = html.encode('utf-8', 'surrogateescape')
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(encoded)))
        self.end_headers()

        # Write the encoded HTML directly to wfile
        self.wfile.write(encoded)

    def format_size(self, size):
        """Convert file size to human-readable format"""
        for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
            if size < 1024:
                return f"{size:.2f} {unit}"
            size /= 1024
        return f"{size:.2f} TB"
for method in ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'OPTIONS']:
    if hasattr(CustomHTTPRequestHandler, 'do_' + method):
        original_method = getattr(CustomHTTPRequestHandler, 'do_' + method)
        setattr(CustomHTTPRequestHandler, 'do_' + method, 
                CustomHTTPRequestHandler.require_auth(original_method))

class WebDAVRequestHandler(CustomHTTPRequestHandler):
    _auth_enabled = False
    _credentials = ('', '')  # 存储(user, password)
    


    def require_auth(f):
        """身份验证装饰器"""
        @wraps(f)
        def wrapper(self, *args, **kwargs):
            if not self._auth_enabled:
                return f(self, *args, **kwargs)
                
            auth_header = self.headers.get('Authorization', '')
            if not auth_header.startswith('Basic '):
                self._send_auth_challenge()
                return

            try:
                # 解析Base64凭证
                auth_decoded = base64.b64decode(auth_header[6:]).decode('utf-8')
                user, passwd = auth_decoded.split(':', 1)
                if (user, passwd) == self._credentials:
                    return f(self, *args, **kwargs)
            except Exception:
                pass
            
            self._send_auth_challenge()
        return wrapper

    def _send_auth_challenge(self):
        """发送401认证质询"""
        self.send_response(401)
        self.send_header('WWW-Authenticate', 'Basic realm="shenmo\'s WebDAV Server", charset="UTF-8"')
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(b'<h1>Authentication Required</h1>')


    def do_OPTIONS(self):
        """确保OPTIONS响应包含PROPPATCH支持"""
        self.send_response(200)
        self.send_header('Allow', ', '.join([
            'OPTIONS', 'GET', 'HEAD', 'POST', 
            'PUT', 'DELETE', 'MKCOL', 'PROPFIND',
            'PROPPATCH', 'LOCK', 'UNLOCK',       # 新增PROPPATCH支持
            'MOVE', 'COPY'
        ]))
        self.send_header('DAV', '1, 2')
        self.send_header('Content-Length', '0')
        self.end_headers()

    def do_PROPPATCH(self):
        """处理PROPPATCH请求以实现WebDAV属性修改"""
        path = self.translate_path(self.path)
        if not os.path.exists(path):
            self.send_error(404, "File not found")
            return

        # 读取请求XML内容
        try:
            content_length = int(self.headers.get('Content-Length', 0))
            raw_xml = self.rfile.read(content_length)
            root = ET.fromstring(raw_xml)
        except (ValueError, ET.ParseError) as e:
            self.send_error(400, f"Invalid XML: {str(e)}")
            return
        
        # 验证XML根元素
        if root.tag != '{DAV:}propertyupdate':
            self.send_error(400, "Root element must be propertyupdate")
            return

        # 构造多状态响应XML
        multistatus = ET.Element('{DAV:}multistatus')
        response = ET.SubElement(multistatus, '{DAV:}response')
        href = ET.SubElement(response, '{DAV:}href')
        href.text = self.path

        # 处理属性更新请求
        propstat = ET.SubElement(response, '{DAV:}propstat')
        prop = ET.SubElement(propstat, '{DAV:}prop')
        status = ET.SubElement(propstat, '{DAV:}status')
        
        result = {'code': 200, 'message': 'OK'}
        
        # 遍历所有属性操作
        for operation in root:
            if operation.tag == '{DAV:}set':
                target_prop = operation.find('.//{DAV:}prop')
                for elem in list(target_prop):
                    status_code = self._process_property(path, elem)
                    result['code'] = max(result['code'], status_code)
            elif operation.tag == '{DAV:}remove':
                # 属性移除暂不支持（示例代码）
                result['code'] = 403
                ET.SubElement(prop, elem.tag)  # 记录失败属性

        # 设置最终响应状态
        status.text = f'HTTP/1.1 {result["code"]} {result["message"]}'
        
        # 生成XML响应
        response_xml = ET.tostring(multistatus, encoding='utf-8', method='xml')
        self.send_response(207)
        self.send_header('Content-Type', 'text/xml; charset="utf-8"')
        self.send_header('Content-Length', len(response_xml))
        self.end_headers()
        self.wfile.write(response_xml)

    def _process_property(self, path, elem):
        """处理单个属性（返回HTTP状态码）"""
        if elem.tag == '{DAV:}displayname':
            # 在此不做文件系统的重命名，仅表示 displayname 已接收
            return 200  # 只表示 displayname 更新成功
        elif elem.tag == '{DAV:}getlastmodified':
            # 返回成功
            return 200
        # 对于其他未实现的属性返回501
        return 501



    
    def do_PROPFIND(self):
        path = self.translate_path(self.path)
        if not os.path.exists(path):
            self.send_error(404, "File not found")
            return

        depth = self.headers.get('Depth', '1')
        
        # 生成XML内容
        xml_content = []
        xml_content.append('<?xml version="1.0" encoding="utf-8"?>')
        xml_content.append('<d:multistatus xmlns:d="DAV:">')
        
        # 添加当前路径
        self._add_propfind_response(xml_content, self.path, path)
        
        # 处理子目录
        if os.path.isdir(path) and depth != '0':
            for item in os.listdir(path):
                item_path = os.path.join(path, item)
                item_url = self.path.rstrip('/') + '/' + item
                # 确保URL路径使用正斜杠
                item_url = item_url.replace('\\', '/')  # Windows路径转换
                self._add_propfind_response(xml_content, item_url, item_path)
        
        xml_content.append('</d:multistatus>')
        full_xml = '\n'.join(xml_content).encode('utf-8')
        
        # 发送响应
        self.send_response(207)
        self.send_header('Content-Type', 'text/xml; charset="utf-8"')
        self.send_header('Content-Length', len(full_xml))
        self.end_headers()
        self.wfile.write(full_xml)

    def _add_propfind_response(self, xml_content, url, path):
        """生成完整的PROPFIND响应XML（含所有标准属性）"""
        # 转义XML特殊字符
        escaped_url = escape(url)
        
        # 基础文件信息
        is_dir = os.path.isdir(path)
        file_name = os.path.basename(path.rstrip('/'))
        escaped_name = escape(file_name)
        
        # 获取时间戳
        stat = os.stat(path)
        mtime = stat.st_mtime      # 最后修改时间
        ctime = stat.st_ctime      # 创建时间（受平台限制）
        
        # 格式化时间
        last_modified = time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(mtime))
        creation_date = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(ctime))
        
        # 生成ETag（简化版：inode + 修改时间 + 文件大小）
        etag = f'"{hex(stat.st_ino)[2:]}-{int(mtime)}-{stat.st_size}"' if not is_dir else '""'

        # XML内容构建
        xml_content.extend([
            '<d:response>',
            f'<d:href>{escaped_url}</d:href>',
            '<d:propstat>',
            '<d:prop>',
            
            # --- Resource Type ---
            '<d:resourcetype>{}'.format('<d:collection/>' if is_dir else '') + '</d:resourcetype>',
            
            # --- Content Length ---
            '<d:getcontentlength>{}</d:getcontentlength>'.format(stat.st_size) if not is_dir else '',
            
            # --- Last Modified Time ---
            f'<d:getlastmodified>{last_modified}</d:getlastmodified>',
            
            # --- Creation Date ---
            f'<d:creationdate>{creation_date}</d:creationdate>',
            
            # --- Display Name ---
            f'<d:displayname>{escaped_name}</d:displayname>',
            
            # --- Content Type ---
            '<d:getcontenttype>{}</d:getcontenttype>'.format(
                mimetypes.guess_type(file_name)[0] or ('httpd/unix-directory' if is_dir else 'application/octet-stream')
            ),
            
            # --- ETag ---
            f'<d:getetag>{etag}</d:getetag>',
            
            # --- Supported Lock (基本实现) ---
            '''<d:supportedlock>
                <d:lockentry>
                    <d:lockscope><d:exclusive/></d:lockscope>
                    <d:locktype><d:write/></d:locktype>
                </d:lockentry>
                <d:lockentry>
                    <d:lockscope><d:shared/></d:lockscope>
                    <d:locktype><d:write/></d:locktype>
                </d:lockentry>
            </d:supportedlock>''',
            
            '</d:prop>',
            '<d:status>HTTP/1.1 200 OK</d:status>',
            '</d:propstat>',
            '</d:response>'
        ])



    # WebDAV MKCOL method to create a collection (directory)
    def do_MKCOL(self):
        path = self.translate_path(self.path)
        if os.path.exists(path):
            self.send_error(405, "Method Not Allowed")
            return

        os.makedirs(path)
        self.send_response(201)
        self.end_headers()

    # WebDAV DELETE method to delete a file or directory
    def do_DELETE(self):
        path = self.translate_path(self.path)
        if not os.path.exists(path):
            self.send_error(404, "File not found")
            return

        if os.path.isdir(path):
            shutil.rmtree(path)
        else:
            os.remove(path)

        self.send_response(204)
        self.end_headers()

    def do_PUT(self):
        """
        处理 WebDAV 上传（PUT方法），支持:
          1) 非分块 (Content-Length)
          2) 分块 (Transfer-Encoding: chunked)
        """
        path = self.translate_path(self.path)
        # 确保上传目录存在
        os.makedirs(os.path.dirname(path), exist_ok=True)

        # 判断是否是 chunked 方式
        transfer_encoding = self.headers.get('Transfer-Encoding', '').lower()
        content_length = self.headers.get('Content-Length')

        try:
            if 'chunked' in transfer_encoding:
                # --- Chunked 方式 ---
                self._handle_chunked_put(path)
            elif content_length is not None:
                # --- 普通方式（Content-Length） ---
                self._handle_content_length_put(path, int(content_length))
            else:
                # 两种都没有，就会报错
                self.send_error(400, "Missing Content-Length or Transfer-Encoding: chunked")
                return

            self.send_response(201, "Created")
            self.end_headers()
        except Exception as e:
            self.send_error(500, f"Internal Server Error: {str(e)}")

    def _handle_content_length_put(self, filepath, content_length):
        """基于 Content-Length 的上传"""
        with open(filepath, "wb") as f:
            remaining = content_length
            buffer_size = 8192
            while remaining > 0:
                chunk_size = min(buffer_size, remaining)
                data = self.rfile.read(chunk_size)
                if not data:
                    break
                f.write(data)
                remaining -= len(data)

    def _handle_chunked_put(self, filepath):
        """基于 Transfer-Encoding: chunked 的上传"""
        with open(filepath, "wb") as f:
            while True:
                # 读取一行，获取 chunk size（十六进制）
                chunk_size_line = self.rfile.readline().strip()
                if not chunk_size_line:
                    # 空行或者异常
                    break

                # 转成10进制
                try:
                    chunk_size = int(chunk_size_line, 16)
                except ValueError:
                    # 如果无法解析为16进制，客户端传输格式不对
                    raise ValueError("Invalid chunk size header")

                if chunk_size == 0:
                    # chunk_size = 0 表示结束，还要读掉可能的 trailer 部分
                    # 读取并丢弃剩下的header（直到遇到空行）
                    _ = self.rfile.readline()
                    break

                # 读取 chunk_size 字节的数据
                chunk_data = self.rfile.read(chunk_size)
                if not chunk_data:
                    break
                f.write(chunk_data)

                # chunk 后面还有一个空行(CRLF)需要读掉
                _ = self.rfile.readline()


        
    
    locks = {}  # Dictionary to store lock information

    # WebDAV LOCK method
    def do_LOCK(self):
        path = self.translate_path(self.path)
        
        # If the resource is already locked, return an error
        if path in self.locks:
            self.send_response(423)  # 423 Locked
            self.end_headers()
            return

        # Generate a lock token and store it
        lock_token = str(uuid.uuid4())
        self.locks[path] = {
            'token': lock_token,
            'owner': self.headers.get('Owner'),  # Optional owner info from client
            'timeout': 3600  # Example timeout in seconds
        }

        # Send lock response
        self.send_response(200)
        self.send_header('Content-Type', 'application/xml; charset="utf-8"')
        self.send_header('Lock-Token', f'<urn:uuid:{lock_token}>')
        self.end_headers()

        lock_response = f'''<?xml version="1.0" encoding="utf-8"?>
        <d:prop xmlns:d="DAV:">
            <d:lockdiscovery>
                <d:activelock>
                    <d:locktoken>
                        <d:href>urn:uuid:{lock_token}</d:href>
                    </d:locktoken>
                </d:activelock>
            </d:lockdiscovery>
        </d:prop>'''
        self.wfile.write(lock_response.encode('utf-8'))

    # WebDAV UNLOCK method
    def do_UNLOCK(self):
        path = self.translate_path(self.path)
        lock_token = self.headers.get('Lock-Token')

        # Verify that the token matches
        if path in self.locks and self.locks[path]['token'] == lock_token.strip('<>'):
            del self.locks[path]  # Remove the lock
            self.send_response(204)  # No Content
        else:
            self.send_response(400)  # Bad Request
        self.end_headers()
    # WebDAV MOVE method
    def do_MOVE(self):
        path = self.translate_path(self.path)

    # 解析 Destination 头中的路径部分
        destination = self.headers.get('Destination')
        if not destination:
            self.send_error(400, "Bad Request: Missing Destination header")
            return

        # 提取目标路径并解码
        parsed_dest = urlparse(destination)
        dest_path = unquote(parsed_dest.path)  # 处理中文路径
        full_dest_path = self.translate_path(dest_path)

    # 检查目标路径是否已存在及是否覆盖
        overwrite = self.headers.get('Overwrite', 'T').upper()
        original_dest_exists = os.path.exists(full_dest_path)

        if original_dest_exists:
            if overwrite == 'F':
                self.send_error(412, "Precondition Failed: Destination exists and Overwrite is False")
                return
            else:
                # 如果目标路径已存在且需要覆盖，删除目标路径
                try:
                    if os.path.isdir(full_dest_path):
                        shutil.rmtree(full_dest_path)
                    else:
                        os.remove(full_dest_path)
                except Exception as e:
                    self.send_error(500, f"Failed to remove existing destination: {str(e)}")
                    return      



        try:
            shutil.move(path, full_dest_path)  # normal file move
            
        # Response code depending on whether the destination was overwritten or not
            response_code = 204 if original_dest_exists else 201
            self.send_response(response_code)
            self.end_headers()
        except Exception as e:
            self.send_error(500, f"Move failed: {str(e)}")  


    
# WebDAV COPY method
    def do_COPY(self):
        path = self.translate_path(self.path)
        destination = self.headers.get('Destination')
        overwrite = self.headers.get('Overwrite', 'T')  # Default to True if not    specified
    
        if not destination:
            self.send_error(400, "Bad Request: Missing Destination header")
            return
    
        dest_path = self.translate_path(destination)
    
        if os.path.exists(dest_path):
            if overwrite == 'F':
                self.send_error(412, "Precondition Failed: Destination exists and Overwrite is False")
                return
            else:
            # Overwrite existing destination
                if os.path.isdir(dest_path):
                    shutil.rmtree(dest_path)
                else:
                    os.remove(dest_path)
    
        if os.path.isdir(path):
            shutil.copytree(path, dest_path)
        else:
            shutil.copy2(path, dest_path)
    
        self.send_response(201 if not os.path.exists(dest_path) else 204)
        self.end_headers()

auth_required_methods = [
    'GET', 'OPTIONS' ,'PROPPATCH', 'MKCOL', 
    'DELETE', 'MOVE', 'COPY',
    'LOCK', 'UNLOCK', 'PUT', 'POST'
]

for method in auth_required_methods:
    if hasattr(WebDAVRequestHandler, 'do_' + method):
        setattr(WebDAVRequestHandler, 'do_' + method, 
                WebDAVRequestHandler.require_auth(getattr(WebDAVRequestHandler, 'do_' + method))
        )
    
def start_http_server(port, address, handler_class, server_class, directory):
    if address == '':
        address = '::'
    server_address = (address, port)

    httpd = server_class(server_address, handler_class)

    # 打印启动信息，显示正确的 handler 类名
    print("="*50)
    print(f"Starting server on:")
    print(f"  Address : {address}")
    print(f"  Port    : {port}")
    print(f"  Root    : {directory}")
    print(f"  Handler : {handler_class.__name__}")
    print(f'-------->> Now, listening at port {port}...')

    # List all IP addresses
    print('-------->> Available IP addresses:')
    for interface, ip, version in list_all_ips():
        print(f"-------->> {interface} ({version}): http://{ip}:{port}")
    print("="*50)

    httpd.serve_forever()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Run a simple HTTP or WebDAV server.')
    parser.add_argument('port', type=int, nargs='?', default=8000, help='Port to listen on (default: 8000)')
    parser.add_argument('-b', '--bind', default='', help='Address to bind to (default: 0.0.0.0)')
    parser.add_argument('-d', '--directory', default='.', help='Directory to serve (default: current directory)')
    parser.add_argument('-p', '--protocol', choices=['http', 'webdav'], default='http', help='Protocol version (default: http)')
    parser.add_argument('--cgi', action='store_true', help='Enable CGI')
    parser.add_argument('--auth', help='Authentication credentials (user:pass)')

    args = parser.parse_args()

    # 处理认证参数
    if args.auth:
        if ':' not in args.auth:
            print("错误：认证参数格式应为 user:password")
            exit(1)
        if args.protocol == 'http':
            CustomHTTPRequestHandler._credentials = tuple(args.auth.split(':', 1))
            CustomHTTPRequestHandler._auth_enabled = True
        elif args.protocol == 'webdav':
            WebDAVRequestHandler._credentials = tuple(args.auth.split(':', 1))
            WebDAVRequestHandler._auth_enabled = True

    if args.protocol == 'http':
        # Change working directory to the one specified by the -d option
        os.chdir(args.directory)

        if args.cgi:
            handler_class = http.server.CGIHTTPRequestHandler
            handler_class.cgi_directories = {"/cgi-bin": "/cgi-bin"}  # Adjust as needed
        else:
            handler_class = CustomHTTPRequestHandler

        start_http_server(args.port, address=args.bind, handler_class=handler_class, server_class=ForkingHTTPServer, directory=args.directory)

    elif args.protocol == 'webdav':
        os.chdir(args.directory)
        start_http_server(args.port, address=args.bind, handler_class=WebDAVRequestHandler, server_class=ForkingHTTPServer, directory=args.directory)
